package bean_factory

import (
	"errors"
	"fmt"
	"gitee.com/kristas/booting-go/framework/common/util/list"
	"gitee.com/kristas/booting-go/framework/common/util/reflectx"
	"gitee.com/kristas/booting-go/framework/core/configure"
	"reflect"
)

const InjectFieldTag = "wire"

type injectProcessor interface {
	Filter(field reflect.StructField) bool
	Inject(field reflect.StructField, value reflect.Value) (err error)
}

var injectors []injectProcessor

func init() {
	injectors = []injectProcessor{
		new(beanTagInjector),
		new(emptyTagPtrInjector),
		new(emptyTagInterfaceInjector),
		new(emptyTagInterfaceSliceInjector),
	}
}

func BindProperties(b interface{}) error {
	err := createBeanInstance(b)
	if err != nil {
		return err
	}
	err = configure.Bind(b)
	if err != nil {
		return err
	}
	return nil
}

func createBeanInstance(bean interface{}) error {
	return reflectx.ForEachField(bean, true, func(field reflect.StructField, value reflect.Value) error {
		err := runInjectProcessor(injectors, field, value)
		if err != nil {
			return errors.New(fmt.Sprintf("Inject value in %s failed: field[name=%s, type=%s]\ncaused by: %s", reflect.TypeOf(bean).String(), getFieldBeanName(field), field.Type.String(), err.Error()))
		}
		return nil
	})
}

func runInjectProcessor(processors []injectProcessor, field reflect.StructField, value reflect.Value) error {
	_, ok := field.Tag.Lookup(InjectFieldTag)
	if !ok {
		return nil
	}
	i, find := list.NewList(processors).FindBy(func(i int) bool {
		return processors[i].Filter(field)
	})
	if !find {
		return errors.New("could not match any inject rule")
	}
	return processors[i].Inject(field, value)
}

/*
- Inject_Type: inject by name
- Inject_Rule:
- field is exported
- field is pointer or interface
- field has InjectFieldTag tag, and is not empty
*/
type beanTagInjector struct{}

func (b *beanTagInjector) Filter(field reflect.StructField) bool {
	return !isInjectTagEmpty(field) && //ruleTagNotEmpty
		(field.Type.Kind() == reflect.Ptr || field.Type.Kind() == reflect.Interface)
}

func (b *beanTagInjector) Inject(field reflect.StructField, value reflect.Value) error {
	return setFieldValue(field, value)
}

/*
- Inject_Type: inject by type
- Inject_Rule:
- field is exported
- field is pointer
- field has InjectFieldTag tag, and is empty
*/
type emptyTagPtrInjector struct{}

func (b *emptyTagPtrInjector) Filter(field reflect.StructField) bool {
	return isInjectTagEmpty(field) && //ruleEmptyTag
		field.Type.Kind() == reflect.Ptr //rulePointer
}

func (b *emptyTagPtrInjector) Inject(field reflect.StructField, value reflect.Value) error {
	return setFieldValue(field, value)
}

/*
- Inject_Type: inject by interface
- Inject_Rule:
- field is exported
- field is interface
- field has InjectFieldTag tag, and is empty
*/
type emptyTagInterfaceInjector struct{}

func (i *emptyTagInterfaceInjector) Filter(field reflect.StructField) bool {

	return isInjectTagEmpty(field) && //ruleEmptyTag
		field.Type.Kind() == reflect.Interface //ruleInterface
}

func (i *emptyTagInterfaceInjector) Inject(field reflect.StructField, value reflect.Value) (err error) {
	beans := GetApplicationContext().GetBeansByInterfaceType(field.Type)
	if len(beans) == 0 {
		return errors.New(fmt.Sprintf("could not find bean who implements: %s", field.Type.String()))
	}
	var values = reflect.ValueOf(beans[0])
	value.Set(values)
	return nil
}

/*
- Inject_Type: inject by interface slice
- Inject_Rule:
- field is exported
- field is interface slice
- field has InjectFieldTag tag, and is empty
*/
type emptyTagInterfaceSliceInjector struct{}

func (s *emptyTagInterfaceSliceInjector) Filter(field reflect.StructField) bool {
	return isInjectTagEmpty(field) && //ruleEmptyTag
		field.Type.Kind() == reflect.Slice && field.Type.Elem().Kind() == reflect.Interface //ruleSliceInterface
}

func (s *emptyTagInterfaceSliceInjector) Inject(field reflect.StructField, value reflect.Value) (err error) {
	beans := GetApplicationContext().GetBeansByInterfaceType(field.Type.Elem())
	var values []reflect.Value
	for _, bean := range beans {
		values = append(values, reflect.ValueOf(bean))
	}
	value.Set(reflect.Append(value, values...))
	return nil
}

func setFieldValue(field reflect.StructField, value reflect.Value) error {
	var beanName = getFieldBeanName(field)
	beanValue, err := GetApplicationContext().GetBean(beanName)
	if err != nil {
		return err
	}
	value.Set(reflect.ValueOf(beanValue))
	return nil
}

func isInjectTagEmpty(field reflect.StructField) bool {
	value, _ := field.Tag.Lookup(InjectFieldTag)
	return value == ""
}

func getFieldBeanName(field reflect.StructField) string {
	var beanName string
	if beanName = field.Tag.Get(InjectFieldTag); beanName == "" {
		if field.Type.Kind() == reflect.Ptr {
			beanName = field.Type.Elem().String()
		} else {
			beanName = field.Type.String()
		}
	}
	return beanName
}
